﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Data;
using System.Text.RegularExpressions;

namespace Xeot.Library.Nacha
{
	/// <summary>
	/// 'Our' or 'Company' means the destination for the money, who is running this class.
	/// 'Customer' means the person or entity we are collecting money from (debits) or paying money to (credits).
	/// </summary>
    public class NachaFile 
    {
		private string _cns; //set in the constructor
		private string _sReserved = "  "; //Two blank spaces
		private string _sAddendaRecordIndicator = "0"; //0 = no addenda supplied
		private string _sPriorityCode = "01"; //always 01?
		private DateTime _dtFileCreationDateTime = DateTime.Now;

		private decimal _dTotalDebits = 0.0m;
		private decimal _dTotalCredits = 0.0m;
		private int _iRoutingHash = 0;
		private int _iTotalDetailRecords = 0;
		private int _iTransactionNumber = 0;

		private string _sRecordTypeFileHeaderLine = "1";
		private string _sRecordTypeCompanyHeaderLine = "5";
		private string _sRecordTypeDetailLine = "6";
		private string _sRecordTypeCompanyFooterLine = "8";
		private string _sRecordTypeFileControlLine = "9";

		private string _sBytesPerRecord = "094";
		private string _sRecordsPerBlock = "10";

		private string _sACHType = "PPD"; //Prearranged payments and deposit
		private string _sServiceClass = "200";
		private string _sOriginatorStatusCode = "1"; //Originator Status Code

		StringBuilder _sbOurBigFile;
		
		//These values should all be set through the ui:
		private string _sOurCompanyNameLong = "";
		private string _sOurCompanyNameShort = "";
		private string _sOurRoutingNumber = ""; //last digit is the check digit
		private string _sOurAccountNumber = "";
		private string _sImmediateDestination = "";
		private string _sImmediateOrigin = "";
		private string _sOurBankName = "";
		private string _sOurReferenceCode = "";
		private string _sOurDiscretionaryData = "";
		private string _sEntryDescription = "";
		private string _sDailyBatchNumber = ""; //Our batch number for this day (1 = first batch of the day)

		/// <summary>
		/// Returns the stringbuilder obj with our NACHA file.
		/// </summary>
		public StringBuilder OurBigFile
		{
			get { return _sbOurBigFile; }
		}

		/// <summary>
		/// Set our company name this may be trimmed to fit. Max 23.
		/// </summary>
		public string OurCompanyNameLong
		{
			set { _sOurCompanyNameLong = value; }
		}

		/// <summary>
		/// Set our company name this may be trimmed to fit. Max 16.
		/// </summary>
		public string OurCompanyNameShort
		{
			set { _sOurCompanyNameShort = value; }
		}
		
		/// <summary>
		/// 9 digits. 8 for the routing number + 1 for the check digit.
		/// </summary>
		public string OurRoutingNumber
		{
			set { _sOurRoutingNumber = value; }
		}
		
		/// <summary>
		/// Our destination bank account number. The length of our account number will vary but must be less than XX.
		/// </summary>
		public string OurAccountNumber
		{
			set { _sOurAccountNumber = value; }
		}

		/// <summary>
		/// Our bank name
		/// </summary>
		public string OurBankName
		{
			set
			{
				_sOurBankName = value;
			}
		}

		/// <summary>
		/// Our reference code
		/// </summary>
		public string OurReferenceCode
		{
			set
			{
				_sOurReferenceCode = value;
			}
		}

		/// <summary>
		/// Our daily batch number, e.g. this number x of however many batches we're running today.
		/// </summary>
		public string DailyBatchNumber
		{
			set
			{
				_sDailyBatchNumber = value;
			}
		}

		/// <summary>
		/// This definitely something
		/// </summary>
		public string OurDiscretionaryData
		{
			set
			{
				_sOurDiscretionaryData = value;
			}
		}

		/// <summary>
		/// This might be the line description of the charge on the customers statement?
		/// </summary>
		public string EntryDescription
		{
			set
			{
				_sEntryDescription = value;
			}
		}

		/// <summary>
		/// The immdediate destination of the generated file. Ex: 121108250 = SF BoA. Should be 9 characters.
		/// </summary>
		public string ImmediateDestination
		{
			set
			{
				//Prepending space per BoA spec:
				_sImmediateDestination = " " + value;
			}
		}
		
		/// <summary>
		/// Our company number assigned to us by our bank. This is who is sending this generated file. Should be 10 characters.
		/// </summary>
		public string ImmediateOrigin
		{
			set { _sImmediateOrigin = value; }
		}

		/// <summary>
		/// Das Constructor
		/// </summary>
		/// <param name="ConnectionString"></param>
		public NachaFile(string ConnectionString)
        {
            _cns = ConnectionString;
        }

		public bool GenerateFile()
        {
			_sbOurBigFile = new StringBuilder();
			_sbOurBigFile.Append(BuildFileHeaderLine());
			_sbOurBigFile.Append(BuildCompanyHeaderLine());
			_sbOurBigFile.Append(BuildOurDetailLines());
			_sbOurBigFile.Append(BuildCompanyFooterLine());
			_sbOurBigFile.Append(BuildFileControlLine());
            return true;
        }

		/// <summary>
		/// This is the file header line.
		/// </summary>
		/// <returns></returns>
		private string BuildFileHeaderLine()
		{
			StringBuilder sReturnValue = new StringBuilder();
			sReturnValue.Append(_sRecordTypeFileHeaderLine); //Record type code 1=header
			sReturnValue.Append(_sPriorityCode);
			sReturnValue.Append(_sImmediateDestination);
			sReturnValue.Append(_sImmediateOrigin);
			sReturnValue.Append(DateTime.Now.ToString("yyMMdd"));
			sReturnValue.Append(DateTime.Now.ToString("hhmm"));
			sReturnValue.Append("1"); //File Number
			sReturnValue.Append(_sBytesPerRecord); //number of bytes/record
			sReturnValue.Append(_sRecordsPerBlock); //number of records/block
			sReturnValue.Append("1"); //format code
			sReturnValue.Append(FormatText(_sOurBankName, 23)); //bank name 23 chars
			sReturnValue.Append(FormatText(_sOurCompanyNameLong, 23)); //our name name 23 chars
			sReturnValue.Append(FormatText(_sOurReferenceCode, 8)); //our reference code (optional)
			sReturnValue.AppendLine();
			return sReturnValue.ToString();
		}

		/// <summary>
		/// This is the company header line. A NACHA file can have multiple companies. This class only supports one company.
		/// </summary>
		/// <returns>94 char string</returns>
		private string BuildCompanyHeaderLine()
		{
			StringBuilder sReturnValue = new StringBuilder();
			sReturnValue.Append(_sRecordTypeCompanyHeaderLine);
			sReturnValue.Append(_sServiceClass);
			sReturnValue.Append(FormatText(_sOurCompanyNameShort, 16)); 
			sReturnValue.Append(FormatText(_sOurDiscretionaryData, 20)); 
			sReturnValue.Append(_sImmediateOrigin);
			sReturnValue.Append(_sACHType);
			sReturnValue.Append(FormatText(_sEntryDescription, 10));
			sReturnValue.Append(_dtFileCreationDateTime.ToString("yyMMdd")); //company descriptive date
			sReturnValue.Append(_dtFileCreationDateTime.ToString("yyMMdd")); //effective entry date
			sReturnValue.Append("   "); //Settlement date
			sReturnValue.Append(_sOriginatorStatusCode);
			sReturnValue.Append(_sOurRoutingNumber.Substring(0, 8)); //Minus the check digit
			sReturnValue.Append(FormatNumber(_sDailyBatchNumber, 7));
			sReturnValue.AppendLine();
			return sReturnValue.ToString();
		}

		/// <summary>
		/// Populates a datatable with debit/credit info. 
		/// Iterates through the table adding rows to the return string.
		/// </summary>
		/// <returns></returns>
		private string BuildOurDetailLines()
		{
			StringBuilder sbReturnValue = new StringBuilder();
			DataTable dtDetails = GetMyData();
			foreach (DataRow dr in dtDetails.Rows)
			{
				//Detail record specifier is 6:
				sbReturnValue.Append(_sRecordTypeDetailLine);

				//Checking or Savings:
				if (Convert.ToInt16(dr["CustomerAccountIsSavings"]) == 1)
					sbReturnValue.Append("37");
				else
					sbReturnValue.Append("27");

				//8 chars always, no check digit:
				sbReturnValue.Append(Convert.ToString(dr["CustomerRoutingNumber"]));
				_iRoutingHash += Convert.ToInt32(Convert.ToString(dr["CustomerRoutingNumber"]).Substring(0, 8));

				//17 chars, padded right with spaces
				sbReturnValue.Append(FormatText((dr["CustomerAccountNumber"]).ToString(), 17));

				//10 Digits: $$$$$$$$¢¢, padded left with 0's
				sbReturnValue.Append(FormatMoney(Convert.ToDecimal(dr["ChargeAmount"]), 10));
				_dTotalDebits += Convert.ToDecimal(dr["ChargeAmount"]);

				//This may appear on customer's bank statement, 15 chars, padded right with spaces
				sbReturnValue.Append(FormatText((dr["CustomerID"]).ToString(), 15));

				//Customer's account name, 22 chars, padded right with spaces
				sbReturnValue.Append(FormatText((dr["CustomerAccountName"]).ToString(), 22));

				sbReturnValue.Append(_sReserved);
				sbReturnValue.Append(_sAddendaRecordIndicator);

				//*Our* routing number, why here? no idea! (first 8 digits, exclude the 9th digit which is the check digit)
				sbReturnValue.Append(_sOurRoutingNumber.Substring(0, 8));

				//Transaction ID is just a running total for this batch, left padding zeroes
				sbReturnValue.Append(FormatNumber(_iTransactionNumber++.ToString(), 7));

				//Add newline
				sbReturnValue.AppendLine();

				//Add one to our detail record counter, this may always be the same value as the _iTransactionNumber variable, not sure.
				_iTotalDetailRecords++;
			}
			return sbReturnValue.ToString();
		}

		/// <summary>
		/// This is the company footer. A NACHA file can have multiple companies. This class only supports one company.
		/// </summary>
		/// <returns>94 char string</returns>
		private string BuildCompanyFooterLine()
		{
			StringBuilder sbReturnValue = new StringBuilder();
			sbReturnValue.Append(_sRecordTypeCompanyFooterLine);
			sbReturnValue.Append(_sServiceClass); //service class
			sbReturnValue.Append(FormatNumber(_iTotalDetailRecords.ToString(), 6)); //Total number of detail records in this file (type 6 records)
			sbReturnValue.Append(FormatNumber(_iRoutingHash.ToString(), 10)); //routing/entry hash
			sbReturnValue.Append(FormatMoney(_dTotalDebits, 12)); //total money to us
			sbReturnValue.Append(FormatMoney(_dTotalCredits, 12)); //total money to them
			sbReturnValue.Append(_sImmediateOrigin); //our company id from bank
			sbReturnValue.Append(FormatText("", 19)); //reserved
			sbReturnValue.Append(FormatText("", 6)); //reserved
			sbReturnValue.Append(_sOurRoutingNumber.Substring(0, 8)); //Bank assigned destination of money. In our case it's our routing number sans check digit
			sbReturnValue.Append(FormatNumber(_sDailyBatchNumber, 7));  //our batch number for this day (1 = first batch of the day)
			sbReturnValue.AppendLine();
			return sbReturnValue.ToString();
		}

		/// <summary>
		/// This is the file footer is in the NACHA spec to support multiple companies.
		/// This class only supports one company so many entries here are duplicate values
		/// from the Company Footer Line.
		/// </summary>
		/// <returns>94 char string plus required filler</returns>
		private string BuildFileControlLine()
		{
			decimal dTotalLineCount = Convert.ToDecimal(_iTotalDetailRecords + 4);
			int iBlockCount = Convert.ToInt32(Math.Ceiling(dTotalLineCount / 10));

			StringBuilder sbReturnValue = new StringBuilder();
			sbReturnValue.Append(_sRecordTypeFileControlLine);
			sbReturnValue.Append(FormatNumber("1", 6));  //Total number of Company Record Header Lines (type 5 records) in the generated file. This class supports 1.
			sbReturnValue.Append(FormatNumber(iBlockCount.ToString(), 6));  //Total number of physical blocks in file including header and control records
			sbReturnValue.Append(FormatNumber(_iTotalDetailRecords.ToString(), 8)); //Total # of detail records in this file (type 6 records) across all companies in generated file.
			sbReturnValue.Append(FormatNumber(_iRoutingHash.ToString(), 10)); //routing hash or entry hash for all records across all companies in generated file.
			sbReturnValue.Append(FormatMoney(_dTotalDebits, 12)); //total money for all companies to us (debit)
			sbReturnValue.Append(FormatMoney(_dTotalCredits, 12)); //total money for all companies to them (credit)
			sbReturnValue.Append(FormatText("", 39)); //39 empty spaces
			sbReturnValue.AppendLine();
			
			//do the filler -- not sure if this is required for all banks
			int iFillersToAdd = (iBlockCount * 10) - Convert.ToInt32(dTotalLineCount);
			for (int i = 0; i < iFillersToAdd; i++)
			{
				sbReturnValue.AppendLine("9".PadRight(94, '9'));
			}
			return sbReturnValue.ToString();
		}

		/// <summary>
		/// Convert to upper, remove all bad chars, pad right with ' ', trim the string if required
		/// </summary>
		/// <param name="sStringToPad"></param>
		/// <param name="iRequiredLength"></param>
		/// <returns>a formatted string</returns>
		private string FormatText(string sStringToFormat, int iRequiredLength)
		{
			string sbReturnValue = sStringToFormat.ToUpper();
			sbReturnValue = Regex.Replace(sbReturnValue, "[^A-Z0-9,.*&,$ -]+", "", RegexOptions.Compiled);
			sbReturnValue = (sbReturnValue.PadRight(iRequiredLength)).Substring(0, iRequiredLength);
			return sbReturnValue;
        }

		/// <summary>
		/// Pad left with '0'
		/// </summary>
		/// <param name="sNumberToFormat"></param>
		/// <param name="iRequiredLength"></param>
		/// <returns>a formatted string</returns>
        private string FormatNumber(string sNumberToFormat, int iRequiredLength)
		{
			return sNumberToFormat.PadLeft(iRequiredLength, '0');
        }

		/// <summary>
		/// Format to $$$$$$$$¢¢, remove all the punctuation, then pad left with '0'
		/// </summary>
		/// <param name="dMoneyToFormat"></param>
		/// <param name="iRequiredLength"></param>
		/// <returns>a formatted string</returns>
		private string FormatMoney(decimal dMoneyToFormat, int iRequiredLength)
		{
			string sbReturnValue = String.Format("{0:C}", dMoneyToFormat);
			sbReturnValue = sbReturnValue.Replace("$", "").Replace(",", "").Replace(".", "").Replace(" ", "");
			return sbReturnValue.PadLeft(iRequiredLength, '0');
        }

        /// <summary>
        /// I commented this out so we can compile.
        /// Alter this function to get your data.
        /// </summary>
        /// <returns></returns>
		private DataTable GetMyData()
		{
			DataTable dt = new DataTable();

//			Library.DAL.Data helper = new Library.DAL.Data(_cns);
//			try
//			{
//				string sSQL = @"SELECT UserFieldsTable.String1 AS CustomerID, AccountTable.Name AS CustomerAccountName, UserFieldsTable.String2 AS CustomerBankName, 
//					UserFieldsTable.String3 AS CustomerRoutingNumber, UserFieldsTable.String4 AS CustomerAccountNumber, UserFieldsTable.Decimal1 AS ChargeAmount, 
//					UserFieldsTable.Bool1 AS CustomerAccountIsSavings
//					FROM            UserFieldsTable INNER JOIN
//					AccountTable ON UserFieldsTable.AccountID = AccountTable.AccountID
//					WHERE        (UserFieldsTable.String4 <> '') AND (AccountTable.AccountType = 9) AND (Decimal1 > 0)";
//				dt = helper.GetDataTable(sSQL, CommandType.Text, "details");
//			}
//			catch (Exception ex)
//			{
//				throw new Exception("Unhandled exception! NachaFile.cs#271");
//			}
//			finally
//			{
//				helper = null;
//			}

			return dt;
		}
    }
}